import _ from 'lodash';
import * as THREE from 'three';
import * as loader from './core/loader.js';
import Base from './core/Base.js';
import Color from '../Color.js';
import Actor from './core/Actor.js';
import Storage from '../Storage.js';
import Sprite from './core/Sprite.js';
import Point from '../Point.js';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { Line2 } from 'three/examples/jsm/lines/Line2.js';
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
// import { MeshLine, MeshLineMaterial } from 'three.meshline';
import { MeshLine, MeshLineMaterial } from './THREE.MeshLine.js'; // THREE.Geometry will be removed from core with r125

class Contours extends Base {
    constructor(root) {
        super();
        this.root = root;
        this.offset = { x: 0, y: 0, z: 0 };
        this.opacity = 1;
        this.group = null;
        // this.modelsGroup = new THREE.Group();
        this._cache = Storage.getCacheStorage('CONTOURS_GLTF');
        this.sprites = [];
        this.meshes = [];
        this.geometries = {}; // get ready to merge
    }

    destory() {
        Actor.clearCached();
        this.root && this.root.scene && this.root.scene.remove(this.group);
        this.group = null;
        super.destory();
    }

    checkGroup() {
        if (!this.group) {
            this.group = new THREE.Group();
            this.group.name = 'contoursGroup';
            this.group.rotation.x = Math.PI / -2;
            this.root.scene.add(this.group);
        }
    }
    setStaticPath(path) {
        this.staticPath = path;
    }

    setOffset({ x = 0, y = 0, z = 0 } = {}) {
        this.offset = { x, y, z };
    }

    setOpacity(val) {
        this.opacity = val;
    }

    async append3DModule(o) {
        let x = o.points[0].x + ((o.m3dOffset && o.m3dOffset.x) || 0),
            y = o.points[0].y + ((o.m3dOffset && o.m3dOffset.y) || 0),
            z = (o.points[0].z || 0) + ((o.m3dOffset && o.m3dOffset.z) || 0),
            h = (o.m3dOffset && o.m3dOffset.h) || 0,
            a = this.opacity;
        let g = await new Actor(this.root).init(o.id, o.m3d, true);
        if (g) {
            g = g.clone();
            g.name = 'G_' + o.id;
            g.rotation.x = Math.PI / 2;
            g.scale.set(o.m3dScale, o.m3dScale, o.m3dScale);
            // console.log('x：', x);
            // console.log('this.offset.x：', this.offset);
            if (!this.offset) return;
            g.position.set((x + this.offset.x) / 1000, (y + this.offset.y) / 1000, z);
            if (h) {
                g.rotation.x += (Math.PI / 180) * h;
                g.rotation.y = Math.PI * h;
            }
            // to solve the problem of rendering black ...
            g.traverse(function (child) {
                //eslint-disable-line
                child.userData.contourId = o.id;
                child.userData.is3d = true;
                if (child.isMesh) {
                    child.name = o.id;
                    child.castShadow = true;
                    child.renderOrder = 99;
                    child.material.transparent = true;
                    child.material.opacity = a || 0.5;
                }
            });
            if (o.label) g.userData.contourLabel = o.label;
            // console.error(g);
            if (this.root && this.root.modelsGroup) this.root.modelsGroup.add(g);
        }

        this.root.resetCaches();
    }

    isPath(o) {
        return (
            (o.points && o.points.length === 2) ||
            (o.graphExt && o.graphExt.tagName === 'path' && !!!o.graphExt['graph-type'])
        );
    }

    getGeometry(o) {
        if (!o.points || !o.points.length || o.points.length < 2) return;
        let splinepts = [];
        o.points.forEach(p => splinepts.push(new THREE.Vector3(p.x + this.offset.x, p.y + this.offset.y, 0)));
        let splineShape = new THREE.Shape()
            .moveTo(o.points[0].x + this.offset.x, o.points[0].y + this.offset.y, 0)
            .setFromPoints(splinepts);
        if (o.graphExt && o.graphExt.tagName === 'circle') {
            let r = parseInt(o.graphExt.r),
                cx = parseInt(o.graphExt.cx) + this.offset.x,
                cy = parseInt(o.graphExt.cy) + this.offset.y;
            splineShape = new THREE.Shape().absarc(cx, cy, r, 0, Math.PI * 2);
        }
        let extrudeSettings = {
            depth: 0,
            bevelEnabled: false,
            // bevelSegments: 2,
            // steps: 2,
            // bevelSize: 1,
            // bevelThickness: 1,
            side: THREE.FrontSide
        };
        if (o.m3dHeight) extrudeSettings.depth = o.m3dHeight;
        // extrudeSettings.depth = -5000;
        // extrudeSettings.depth += Math.random() * extrudeSettings.depth;

        // let geometry = new THREE.ExtrudeBufferGeometry(splineShape, extrudeSettings);
        let geometry = null;
        if (this.isPath(o)) {
            // line
            splinepts = [];
            let cps = o.points;
            if (o.points.length > 2) cps = Point.getClothoidPoints(o.points, 10000) || [];
            cps.forEach(p => splinepts.push(new THREE.Vector3(p.x + this.offset.x, p.y + this.offset.y, 0)));
            geometry = new THREE.BufferGeometry().setFromPoints(splinepts);
        } else if (o.m3dHeight) {
            extrudeSettings.side = THREE.BackSide;
            geometry = new THREE.ExtrudeBufferGeometry(splineShape, extrudeSettings);
        } else {
            // polygon
            geometry = new THREE.ShapeGeometry(splineShape, 8);
        }
        return geometry;
    }
    getMesh(o) {
        if (!o.points || !o.points.length) return;

        let geometry = this.getGeometry(o);
        let color = Color.rgbToHex(o.color);
        let mesh = null;
        // if (o.id === '2262') console.log('---> ', o.id, o.points.length, o.points.length === 2);
        if (this.isPath(o)) {
            // line
            let linewidth = o.graphExt['stroke-width'] || 3000;
            let mate = new THREE.LineBasicMaterial({ color, linewidth });
            mesh = new THREE.Line(geometry, mate);
            mesh.scale.set(0.001, 0.001, 0.001);
            // mesh = this.generateLine(o);
        } else {
            // polygon
            let mate = new THREE.MeshLambertMaterial({ color });
            mesh = new THREE.Mesh(geometry, mate);
            mesh.scale.set(0.001, 0.001, 0.001);
            if (!o.m3dHeight) {
                mesh.material.transparent = false; // flash ...
                mesh.material.depthTest = false;
                // let linewidth = o.graphExt['stroke-width'] || 3000;
                // let mate = new THREE.LineBasicMaterial({ color: 0xff0000, linewidth });
                // let cubeLine = new THREE.Line(geometry, mate);
                // cubeLine.scale.set(0.001, 0.001, 0.001);
                // mesh.add(cubeLine);
                // mesh = cubeLine;
            } else {
                mesh.material.transparent = true;
                mesh.material.depthTest = true;
                // mesh.material.alphaTest = 0.1;
                mate.opacity = this.opacity || 0.5;
                mate.side = THREE.DoubleSide;
                let edges = new THREE.EdgesGeometry(geometry, 1);
                let edgesMtl = new THREE.LineBasicMaterial({ color: 0xffffff });
                let cubeLine = new THREE.LineSegments(edges, edgesMtl);
                mesh.add(cubeLine);
            }
            mesh.receiveShadow = true;
            mesh.traverse(function (child) {
                mesh.userData = _.cloneDeep(o);
                mesh.userData.is3d = false;
                if (child.isMesh) {
                    child.name = o.id;
                }
            });
        }
        if (mesh) {
            this.meshes.push(mesh);
        }
        return mesh;
    }

    /** Deprecated */
    generateLine(o) {
        let positions = [],
            colors = [];
        let cps = Point.getClothoidPoints(o.points) || [];
        cps.forEach(p => positions.push((p.x + this.offset.x) / 1000, (p.y + this.offset.y) / 1000, 0));
        let color = new THREE.Color(o.color);
        colors.push(color.r, color.g, color.b);
        const geometry = new LineGeometry();
        geometry.setPositions(positions);
        geometry.setColors(colors);
        let matLine = new LineMaterial({
            color,
            linewidth: 3, // in pixels
            // vertexColors: true,
            //resolution:  // to be set by renderer, eventually
            dashed: false
        });
        matLine.resolution.set(this.root.viewport.width, this.root.viewport.height); // resolution of the viewport

        let line = new Line2(geometry, matLine);
        line.name = o.id;
        line.computeLineDistances();
        return line;
    }
    async appendChildren(res) {
        this.checkGroup();
        if (!res || !res.length) return;
        for (let i = 0, len = res.length; i < len; i++) {
            let o = res[i];
            // if (o.id === '2071') console.log('----> ', o.id, o.center);
            let g = this.appendChild(o);
            if (g) this.group.add(g);
            // contours labels
            if (o['label']) this.appendLabels(o);
        }
        // // if the contours has 3d model.
        for (let i = 0, len = res.length; i < len; i++) {
            if (res[i] && res[i]['m3d']) await this.append3DModule(res[i]);
        }
    }
    appendChild(o) {
        if (!o || !this.offset) return;
        let mesh = null;
        if (!o.points || !o.points.length) return null;
        mesh = this.getMesh(o);

        if (!mesh) return null;
        // mesh.renderOrder = -1;
        let g = new THREE.Group();
        g.name = o.id;
        g.add(mesh);
        return g;
    }
    encodeKey(o) {
        let color = Color.rgbToHex(o.color) || '#0b0b0b';
        let lineWidth = (o.graphExt && o.graphExt['stroke-width']) || 3000;
        let key = `${o.layer || 0}_${color}_${this.isPath(o) ? lineWidth : ''}_${o.m3dHeight ? '3d' : ''}`;
        return key;
    }
    decodeKey(key) {
        let arr = key.split('_');
        return { layer: arr[0], color: arr[1], isPath: arr[2], is3d: arr[3] };
    }
    getMergedMesh(key) {
        // console.time(`getMergedMesh: ${key}`);
        if (!this.geometries[key] || !this.geometries[key].length) return;
        let { layer, color, isPath, is3d } = this.decodeKey(key);
        let gs = [];
        let transform = new THREE.Object3D();
        this.geometries[key].forEach(o => {
            if (!o.points || !o.points.length) return;
            // if (o.points.length <= 2) return;
            let g = this.getGeometry(o);
            if (!g) return;
            if (isPath) g.lineWidth = (o.graphExt['stroke-width'] || 3000) / 1000;
            gs.push(g);
        });
        // console.timeEnd(`getMergedMesh: ${key}`);
        if (!gs.length) return;
        let mate = null;
        let mesh = null;
        let mergedGeometry = null;
        if (isPath) {
            let mls = [];
            gs.forEach(g => {
                let ml = new MeshLine();
                ml.setGeometry(g);
                mls.push(ml);
            });
            mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(mls);
            mate = new MeshLineMaterial({ color, lineWidth: isPath / 1000 });
            mesh = new THREE.Mesh(mergedGeometry, mate);
        } else if (!is3d) {
            mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(gs);
            mate = new THREE.MeshLambertMaterial({ color });
            // mate = new THREE.MeshBasicMaterial({ color });
            mesh = new THREE.Mesh(mergedGeometry, mate);
            mesh.material.transparent = false; // flash ...
            mesh.material.depthTest = false;
            mesh.receiveShadow = true;
            let currGeometries = this.geometries;
            mesh.traverse(function (child) {
                if (currGeometries && currGeometries[key]) {
                    mesh.userData.data = _.cloneDeep(currGeometries[key]);
                }
                mesh.userData.is3d = false;
            });
            
        } else {
            mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(gs);
            mate = new THREE.MeshLambertMaterial({ color });
            // mate = new THREE.MeshDepthMaterial({ color });
            mesh = new THREE.Mesh(mergedGeometry, mate);
            mesh.material.transparent = true;
            mesh.material.depthTest = true;
            // mesh.material.alphaTest = 0.1;
            mate.opacity = this.opacity || 0.5;
            mate.side = THREE.DoubleSide;
            let edges = new THREE.EdgesGeometry(mergedGeometry, 1);
            let edgesMtl = new THREE.LineBasicMaterial({ color });
            let cubeLine = new THREE.LineSegments(edges, edgesMtl);
            mesh.add(cubeLine);
        }
        mesh.scale.set(0.001, 0.001, 0.001);
        if (mesh) {
            this.meshes.push(mesh);
        }
        return mesh;
    }
    async appendChildrenMerged(res) {
        // return await this.appendChildren(res);
        // console.warn('contours.appendChildrenMerged: ');
        this.checkGroup();
        if (!res || !res.length) return;
        for (let i = 0, len = res.length; i < len; i++) {
            let o = res[i];
            let key = this.encodeKey(o);
            if (!this.geometries[key]) this.geometries[key] = [];
            this.geometries[key].push(o);
            // contours labels
            if (o['label']) this.appendLabels(o);
            // if (key === '3_#fbf5f5__') console.log('=> ', o.id, o);
        }

        if (!this.geometries || !Object.keys(this.geometries).length) return;
        Object.keys(this.geometries).forEach(key => {
            // console.time(`createContours: ${key}`);
            // if (key === '3_#fbf5f5__') console.log('=> ', this.geometries[key].length);
            let mesh = this.getMergedMesh(key);
            if (!mesh) return;
            let g = new THREE.Group();
            g.name = key;
            g.add(mesh);
            this.group.add(g);
            // console.timeEnd(`createContours: ${key}`);
        });
        // // if the contours has 3d model. 
        for (let i = 0, len = res.length; i < len; i++) {
            if (res[i] && res[i]['m3d']) await this.append3DModule(res[i]);
        }
    }

    update(contoursGroup, modelsGroup, res) {
        //eslint-disable-line
        if (!res || !res.length) return;
        // let following = null;
        res.forEach(o => {
            let co = contoursGroup.children.find(v => v.name === o.id);
            if (!co) return;
            let x = (o.points[0].x + this.offset.x) / 1000,
                y = (o.points[0].y + this.offset.y) / 1000;
            // co.position.set(x, y, 0);
            co.moveTo(x, y);

            let mo = modelsGroup.children.find(v => v.name === 'G_' + o.id);
            if (!mo) return;
            x = (o.points[0].x + this.offset.x) / 1000;
            y = (o.points[0].y + this.offset.y) / 1000;
            mo.position.set(x, y, 0);
        });
    }

    // appendTips + removeTips mod by ouyang
    appendTips({
        type = true,
        parentId,
        text,
        color = '#000000',
        position = { x: 0, y: 0, z: 0 },
        offset = {
            x: 0,
            y: 0,
            z: 0
        }
    } = {}) {
        // console.log('Contours.appendTips: ', text, color, position, offset);
        if (!this.root.labelsGroup) return;

        let sprite = new Sprite('contourLabels');
        let ts = '';
        if (type) {
            ts = sprite.getBlackboardMesh(parentId, text);

            ts.rotation.x = -Math.PI / 2;
            ts.position.set(position.x + 5.5, position.y - 1, position.z - 0.5);
        } else if (!type) {
            ts = sprite.getTipsTextSprite(parentId, text);

            ts.rotation.x = -Math.PI / 2;
            ts.position.set(position.x, position.y, position.z + 6);
        }

        this.root.labelsGroup.add(ts); // new THREE.Group().add
        this.sprites.push(ts);
    }
    removeTips() {
        let len = this.sprites.length;
        if (!len) return;
        this.root.labelsGroup.remove(this.sprites[len - 1]); // new THREE.Group().add
        this.sprites.pop();
    }

    appendLabels(o) {
        if (!this.root || !this.root.settings || !this.root.settings.showLabels) return;

        let text = o.label,
            color = o.labelColor,
            position = o.center,
            offset = o.labelOffset;
        position.x = (o.center.x + this.offset.x) / 1000;
        position.y = (o.center.y + this.offset.y) / 1000;
        position.z = 6;
        offset = { x: 0, y: 0, z: 0 };

        // Label. Sprite
        let sprite = new Sprite('contourLabels');
        let ts = sprite.getTextSprite(text);
        ts.rotation.x = -Math.PI / 2;
        ts.position.set(position.x, position.y, position.z + 2);
        this.root.labelsGroup.add(ts);

        // Label. Dash Line
        const lineStyle = {
            color: '#000',
            dashSize: 0.1,
            gapSize: 0.2,
            transparent: true
        };

        let line, dashLine, geometry, point, vertices;
        point = [position.x, position.y, 0, position.x, position.y, position.z + 2];

        vertices = new Float32Array(point);
        geometry = new THREE.BufferGeometry();
        geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
        dashLine = new THREE.LineDashedMaterial(lineStyle);
        line = new THREE.Line(geometry, dashLine);
        line.computeLineDistances();
        this.root.labelsGroup.add(line);
    }

    removeLabels() {
        //
    }

    async loadModel(filename, pos, scaling = 1, offset = { x: 0, y: 0, z: 0 }) {
        this.offset = offset;
        if (!filename || !pos) return;
        // console.log('Contours.loadModel1: ', filename, pos);
        let obj = this._cache.get(filename);
        // console.log('Contours.loadModel2: ', obj);
        if (obj) return this.createModel(obj.clone(), pos, scaling);

        let f = `${this.staticPath}/models/${filename}`;
        obj = await loader.loadGltfAsync(f).catch(e => {
            //eslint-disable-line
            // console.error('===> ', e);
        });
        // console.log('Contours.loadModel: ', filename, obj);
        if (!obj || !obj.scene) return;

        this._cache.set(filename, obj.scene);
        return this.createModel(obj.scene, pos, scaling);
    }

    createModel(obj, pos, scaling) {
        // console.log('Contours.createModel: ', obj, pos);
        obj.rotation.x = Math.PI / 2;
        obj.scale.set(scaling, scaling, scaling);
        // to solve the problem of rendering black ...
        obj.traverse(function (child) {
            //eslint-disable-line
            if (child.isMesh) {
                child.castShadow = true;
                child.renderOrder = 99;
            }
        });
        let group = new THREE.Group();
        group.add(obj);
        group.position.set((pos.x + this.offset.x) / 1000, (pos.y + this.offset.y) / 1000, 0);
        if (pos && pos.h) group.rotation.z = Math.PI * pos.h;
        return group;
        // obj.position.set((pos.x + this.offset.x) / 1000, (pos.y + this.offset.y) / 1000, 0);
        // if (pos && pos.h) obj.rotation.z = Math.PI * pos.h;
        // return obj;
    }
}

export default Contours;
